/*
 * webserver.c — Single-process, multi-connection HTTP static file server using epoll (Linux)
 * Build:   gcc webserver.c -o webserver
 * Run:     ./webserver
 * Browse:  http://localhost:8080/
 *
 * Features:
 * - Single process, non-blocking sockets, epoll (level-triggered)
 * - GET / HEAD (HEAD only sends headers)
 * - Simple POST demo with two-phase read (headers then body)
 * - HTTP/1.1 Keep-Alive
 * - Zero-copy sendfile for file bodies
 * - Basic content-type mapping and directory traversal guard
 * - Standard C comments; braces on every control statement; aligned braces
 */

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#define PORT            8080
#define BACKLOG         128
#define MAX_EVENTS      1024
#define INBUF_SIZE      16384      /* header + small body buffer */
#define OUTBUF_SIZE     1024
#define WEB_ROOT        "web/"

/* -------------------------------
 * Logging (simple, to stderr)
 * ------------------------------- */
static void log_printf(const char *lvl, const char *fmt, ...) {
    time_t t = time(NULL);
    struct tm tmv;
    localtime_r(&t, &tmv);
    char ts[32];
    (void)strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tmv);

    fprintf(stderr, "[%s][%s] ", ts, lvl);
    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
}

/* -------------------------------
 * Helpers
 * ------------------------------- */
static int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags < 0) {
        return -1;
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
        return -1;
    }
    return 0;
}

static const char *guess_content_type(const char *path) {
    const char *ext = strrchr(path, '.');
    if (ext == NULL) { return "text/html"; }
    if (!strcmp(ext, ".html") || !strcmp(ext, ".htm")) { return "text/html; charset=utf-8"; }
    if (!strcmp(ext, ".css"))  { return "text/css; charset=utf-8"; }
    if (!strcmp(ext, ".js"))   { return "application/javascript; charset=utf-8"; }
    if (!strcmp(ext, ".json")) { return "application/json; charset=utf-8"; }
    if (!strcmp(ext, ".png"))  { return "image/png"; }
    if (!strcmp(ext, ".jpg") || !strcmp(ext, ".jpeg")) { return "image/jpeg"; }
    if (!strcmp(ext, ".gif"))  { return "image/gif"; }
    if (!strcmp(ext, ".svg"))  { return "image/svg+xml"; }
    if (!strcmp(ext, ".ico"))  { return "image/x-icon"; }
    if (!strcmp(ext, ".txt"))  { return "text/plain; charset=utf-8"; }
    return "application/octet-stream";
}

static bool contains_dotdot(const char *p) {
    return strstr(p, "..") != NULL;
}

/* -------------------------------
 * Connection state
 * ------------------------------- */
typedef enum {
    ST_RECV_HEADERS = 0,
    ST_RECV_BODY    = 1,
    ST_SEND_HEADERS = 2,
    ST_SEND_FILE    = 3,
    ST_SEND_BUFFER  = 4,
    ST_CLOSED       = 5
} ConnState;

typedef struct {
    int          fd;
    ConnState    state;

    /* input */
    char         inbuf[INBUF_SIZE];
    size_t       in_len;
    bool         header_parsed;
    ssize_t      content_length;    /* -1 if unknown */
    ssize_t      body_received;

    /* request info */
    char         method[16];
    char         path[1024];
    bool         keep_alive;

    /* output headers buffer */
    char         outbuf[OUTBUF_SIZE];
    size_t       out_len;
    size_t       out_sent;

    /* file send info */
    int          file_fd;
    off_t        file_size;
    off_t        file_offset;

    /* response status tracking (for logging) */
    int          resp_status;
} Conn;

/* -------------------------------
 * Request parsing (very simple)
 * ------------------------------- */
static void conn_init(Conn *c, int fd) {
    memset(c, 0, sizeof(*c));
    c->fd              = fd;
    c->state           = ST_RECV_HEADERS;
    c->content_length  = -1;
    c->body_received   = 0;
    c->keep_alive      = true;      /* HTTP/1.1 default keep-alive */
    c->file_fd         = -1;
    c->resp_status     = 0;
}

static void conn_reset_for_next_request(Conn *c) {
    c->state           = ST_RECV_HEADERS;
    c->in_len          = 0;
    c->header_parsed   = false;
    c->content_length  = -1;
    c->body_received   = 0;
    c->out_len         = 0;
    c->out_sent        = 0;
    c->file_size       = 0;
    c->file_offset     = 0;
    if (c->file_fd != -1) {
        close(c->file_fd);
        c->file_fd = -1;
    }
    memset(c->method, 0, sizeof(c->method));
    memset(c->path, 0, sizeof(c->path));
    c->resp_status     = 0;
}

/* Find end of headers: returns pointer to first byte after \r\n\r\n, or NULL if not found */
static char *find_header_end(char *buf, size_t len) {
    if (len < 4) {
        return NULL;
    }
    for (size_t i = 0; i + 3 < len; i++) {
        if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n') {
            return buf + i + 4;
        }
    }
    return NULL;
}

/* Parse request line + minimal headers: method, path, Content-Length, Connection */
static void parse_headers(Conn *c) {
    /* Ensure headers exist */
    char *hdr_end = find_header_end(c->inbuf, c->in_len);
    if (hdr_end == NULL) {
        return;
    }
    c->header_parsed = true;

    /* Request line: METHOD SP PATH SP HTTP/... */
    /* Make a temporary NUL-terminated header block to ease sscanf/strstr */
    size_t header_len = (size_t)(hdr_end - c->inbuf);
    char tmp[INBUF_SIZE];
    if (header_len >= sizeof(tmp)) {
        header_len = sizeof(tmp) - 1;
    }
    memcpy(tmp, c->inbuf, header_len);
    tmp[header_len] = '\0';

    /* Parse request line */
    (void)sscanf(tmp, "%15s %1023s", c->method, c->path);

    /* Connection header (case-sensitive simple search for demo) */
    if (strstr(tmp, "Connection: close") != NULL) {
        c->keep_alive = false;
    } else {
        c->keep_alive = true;
    }

    /* Content-Length */
    char *cl = strstr(tmp, "Content-Length:");
    if (cl != NULL) {
        long v = -1;
        if (sscanf(cl, "Content-Length: %ld", &v) == 1 && v >= 0) {
            c->content_length = v;
        }
    }

    /* Move any body bytes (if already read) to the front portion after headers */
    size_t already_body = (size_t)(c->in_len - (hdr_end - c->inbuf));
    if (already_body > 0) {
        /* Keep body at the end of inbuf; we will count body_received accordingly */
        memmove(c->inbuf, hdr_end, already_body);
    }
    c->in_len = already_body;
    c->body_received = (ssize_t)already_body;
}

/* -------------------------------
 * Response builders
 * ------------------------------- */
static void build_simple_response(Conn *c, int status, const char *status_text,
                                  const char *ctype, const char *body) {
    size_t body_len = (body != NULL) ? strlen(body) : 0;
    c->out_len = (size_t)snprintf(c->outbuf, sizeof(c->outbuf),
                                  "HTTP/1.1 %d %s\r\n"
                                  "Connection: %s\r\n"
                                  "Content-Type: %s\r\n"
                                  "Content-Length: %zu\r\n\r\n",
                                  status, status_text,
                                  c->keep_alive ? "keep-alive" : "close",
                                  ctype, body_len);
    c->out_sent = 0;
    c->resp_status = status;

    if (body_len > 0) {
        /* For small bodies, append into outbuf then use ST_SEND_BUFFER */
        size_t avail = sizeof(c->outbuf) - c->out_len;
        if (body_len <= avail) {
            memcpy(c->outbuf + c->out_len, body, body_len);
            c->out_len += body_len;
            c->state = ST_SEND_BUFFER;
        } else {
            /* Fallback: no large error body here; just send headers */
            c->state = ST_SEND_HEADERS;
        }
    } else {
        c->state = ST_SEND_HEADERS;
    }
}

static void build_file_response(Conn *c, const char *path_fs, off_t fsize, const char *ctype, bool head_only) {
    c->out_len = (size_t)snprintf(c->outbuf, sizeof(c->outbuf),
                                  "HTTP/1.1 200 OK\r\n"
                                  "Connection: %s\r\n"
                                  "Content-Type: %s\r\n"
                                  "Content-Length: %ld\r\n\r\n",
                                  c->keep_alive ? "keep-alive" : "close",
                                  ctype, (long)fsize);
    c->out_sent = 0;
    c->resp_status = 200;

    c->state = ST_SEND_HEADERS;

    if (!head_only) {
        c->file_fd = open(path_fs, O_RDONLY);
        if (c->file_fd >= 0) {
            c->file_size   = fsize;
            c->file_offset = 0;
        } else {
            /* If file open fails after stat OK, return 500 */
            build_simple_response(c, 500, "Internal Server Error",
                                  "text/plain; charset=utf-8", "Internal Server Error");
        }
    }
}

/* -------------------------------
 * Request handling (after full request ready)
 * ------------------------------- */
static void handle_request_ready(Conn *c) {
    /* Only GET/HEAD/POST for demo */
    if (!strcmp(c->method, "GET") || !strcmp(c->method, "HEAD")) {
        char full_path[2048];
        struct stat st;

        if (contains_dotdot(c->path)) {
            build_simple_response(c, 400, "Bad Request",
                                  "text/plain; charset=utf-8", "Bad Request");
            return;
        }

        if (!strcmp(c->path, "/")) {
            (void)snprintf(full_path, sizeof(full_path), "%sIndex.html", WEB_ROOT);
        } else {
            const char *sub = (c->path[0] == '/') ? (c->path + 1) : c->path;
            (void)snprintf(full_path, sizeof(full_path), "%s%s", WEB_ROOT, sub);
        }

        if (stat(full_path, &st) < 0 || S_ISDIR(st.st_mode)) {
            build_simple_response(c, 404, "Not Found",
                                  "text/html; charset=utf-8",
                                  "<!doctype html><html><body><h1>404 Not Found</h1></body></html>");
            return;
        } else {
            const char *ctype = guess_content_type(full_path);
            bool head_only = !strcmp(c->method, "HEAD");
            build_file_response(c, full_path, st.st_size, ctype, head_only);
            return;
        }
    } else if (!strcmp(c->method, "POST")) {
        /* Demo: echo 204 No Content */
        build_simple_response(c, 204, "No Content",
                              "text/plain; charset=utf-8", NULL);
        return;
    } else {
        build_simple_response(c, 405, "Method Not Allowed",
                              "text/plain; charset=utf-8", "Method Not Allowed");
        return;
    }
}

/* -------------------------------
 * EPOLL helpers
 * ------------------------------- */
static void update_epoll_events(int epfd, int fd, uint32_t ev, int op) {
    struct epoll_event e;
    memset(&e, 0, sizeof(e));
    e.events = ev;
    e.data.fd = fd;
    if (epoll_ctl(epfd, op, fd, &e) < 0) {
        perror("epoll_ctl");
    }
}

/* -------------------------------
 * Main
 * ------------------------------- */
int main(void) {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket");
        return 1;
    }
    if (set_nonblocking(listen_fd) < 0) {
        perror("set_nonblocking");
        close(listen_fd);
        return 1;
    }

    int one = 1;
    if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) {
        perror("setsockopt");
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port        = htons(PORT);

    if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(listen_fd);
        return 1;
    }
    if (listen(listen_fd, BACKLOG) < 0) {
        perror("listen");
        close(listen_fd);
        return 1;
    }

    int epfd = epoll_create1(0);
    if (epfd < 0) {
        perror("epoll_create1");
        close(listen_fd);
        return 1;
    }

    update_epoll_events(epfd, listen_fd, EPOLLIN, EPOLL_CTL_ADD);
    log_printf("INFO", "Epoll HTTP server running on port %d (WEB_ROOT=%s)", PORT, WEB_ROOT);
    printf("Access at: http://localhost:%d/\n", PORT);

    /* Map fd -> Conn* with a simple array (could use hashmap). Assume small fd values. */
    Conn *conns[FD_SETSIZE];
    memset(conns, 0, sizeof(conns));

    struct epoll_event events[MAX_EVENTS];

    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (n < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            uint32_t ev = events[i].events;

            if (fd == listen_fd) {
                /* Accept as many as possible */
                while (1) {
                    struct sockaddr_in caddr;
                    socklen_t clen = sizeof(caddr);
                    int cfd = accept(listen_fd, (struct sockaddr *)&caddr, &clen);
                    if (cfd < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;
                        } else {
                            perror("accept");
                            break;
                        }
                    }
                    if (set_nonblocking(cfd) < 0) {
                        perror("set_nonblocking(cfd)");
                        close(cfd);
                        continue;
                    }

                    Conn *c = (Conn *)calloc(1, sizeof(Conn));
                    if (c == NULL) {
                        perror("calloc Conn");
                        close(cfd);
                        continue;
                    }
                    conn_init(c, cfd);
                    conns[cfd] = c;

                    update_epoll_events(epfd, cfd, EPOLLIN | EPOLLRDHUP, EPOLL_CTL_ADD);

                    char ip[INET_ADDRSTRLEN];
                    inet_ntop(AF_INET, &(caddr.sin_addr), ip, INET_ADDRSTRLEN);
                    log_printf("INFO", "Accepted %s:%d (fd=%d)", ip, ntohs(caddr.sin_port), cfd);
                }
                continue;
            }

            Conn *c = conns[fd];
            if (c == NULL) {
                /* Should not happen */
                continue;
            }

            if (ev & (EPOLLHUP | EPOLLERR | EPOLLRDHUP)) {
                /* Peer closed or error */
                c->state = ST_CLOSED;
            }

            if ((ev & EPOLLIN) && c->state <= ST_RECV_BODY) {
                /* Read as much as possible */
                while (1) {
                    ssize_t r = read(fd, c->inbuf + c->in_len, sizeof(c->inbuf) - c->in_len);
                    if (r > 0) {
                        c->in_len += (size_t)r;
                        if (!c->header_parsed) {
                            parse_headers(c);
                            if (c->header_parsed) {
                                /* If POST and need body more */
                                if (!strcmp(c->method, "POST")) {
                                    if (c->content_length < 0) {
                                        c->content_length = 0;
                                    }
                                    if (c->body_received < c->content_length) {
                                        c->state = ST_RECV_BODY;
                                    } else {
                                        /* Body already complete in first read */
                                        c->state = ST_SEND_HEADERS;
                                        handle_request_ready(c);
                                        update_epoll_events(epfd, fd, EPOLLOUT | EPOLLRDHUP, EPOLL_CTL_MOD);
                                        break;
                                    }
                                } else {
                                    /* GET/HEAD: no body expected */
                                    c->state = ST_SEND_HEADERS;
                                    handle_request_ready(c);
                                    update_epoll_events(epfd, fd, EPOLLOUT | EPOLLRDHUP, EPOLL_CTL_MOD);
                                    break;
                                }
                            }
                        } else {
                            /* Header parsed: receiving body if needed */
                            c->body_received = (ssize_t)c->in_len;
                            if (c->body_received >= c->content_length) {
                                c->state = ST_SEND_HEADERS;
                                handle_request_ready(c);
                                update_epoll_events(epfd, fd, EPOLLOUT | EPOLLRDHUP, EPOLL_CTL_MOD);
                                break;
                            }
                        }

                        /* Guard: if input buffer full but body not complete, just truncate/demo */
                        if (c->in_len == sizeof(c->inbuf) && c->state == ST_RECV_BODY &&
                            c->body_received < c->content_length) {
                            /* In a real server, stream body to a file or enlarge buffer */
                            log_printf("WARN", "Request body truncated (demo buffer limit)");
                            c->state = ST_SEND_HEADERS;
                            handle_request_ready(c);
                            update_epoll_events(epfd, fd, EPOLLOUT | EPOLLRDHUP, EPOLL_CTL_MOD);
                            break;
                        }

                        /* Continue reading until EAGAIN */
                        continue;
                    } else if (r == 0) {
                        /* Peer closed */
                        c->state = ST_CLOSED;
                        break;
                    } else {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;
                        } else {
                            perror("read");
                            c->state = ST_CLOSED;
                            break;
                        }
                    }
                }
            }

            if ((ev & EPOLLOUT) && (c->state == ST_SEND_HEADERS || c->state == ST_SEND_BUFFER || c->state == ST_SEND_FILE)) {
                /* 1) Send headers or small body already in outbuf */
                if (c->out_sent < c->out_len) {
                    while (c->out_sent < c->out_len) {
                        ssize_t w = write(fd, c->outbuf + c->out_sent, c->out_len - c->out_sent);
                        if (w > 0) {
                            c->out_sent += (size_t)w;
                            continue;
                        } else if (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
                            break;
                        } else {
                            /* Error */
                            c->state = ST_CLOSED;
                            break;
                        }
                    }
                    if (c->state == ST_CLOSED) {
                        /* Error during header/body buffer write */
                        /* Fall through to cleanup */
                    } else if (c->out_sent < c->out_len) {
                        /* Wait for next EPOLLOUT */
                        /* do nothing */
                    } else {
                        /* Headers/small body done */
                        if (c->state == ST_SEND_BUFFER) {
                            /* Response complete */
                            if (c->keep_alive) {
                                conn_reset_for_next_request(c);
                                update_epoll_events(epfd, fd, EPOLLIN | EPOLLRDHUP, EPOLL_CTL_MOD);
                            } else {
                                c->state = ST_CLOSED;
                            }
                        } else if (c->state == ST_SEND_HEADERS) {
                            /* If we have a file to send (GET 200), switch to SEND_FILE */
                            if (c->file_fd >= 0) {
                                c->state = ST_SEND_FILE;
                            } else {
                                /* No body (HEAD or small response w/o body) */
                                if (c->keep_alive) {
                                    conn_reset_for_next_request(c);
                                    update_epoll_events(epfd, fd, EPOLLIN | EPOLLRDHUP, EPOLL_CTL_MOD);
                                } else {
                                    c->state = ST_CLOSED;
                                }
                            }
                        }
                    }
                }

                /* 2) Send file body via sendfile (non-blocking) */
                if (c->state == ST_SEND_FILE && c->file_fd >= 0) {
                    while (c->file_offset < c->file_size) {
                        off_t remain = c->file_size - c->file_offset;
                        ssize_t sent = sendfile(fd, c->file_fd, &c->file_offset, (size_t)remain);
                        if (sent > 0) {
                            continue;
                        } else if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
                            /* Wait for next EPOLLOUT */
                            break;
                        } else {
                            /* Error */
                            c->state = ST_CLOSED;
                            break;
                        }
                    }
                    if (c->state != ST_CLOSED && c->file_offset >= c->file_size) {
                        /* Done */
                        close(c->file_fd);
                        c->file_fd = -1;
                        if (c->keep_alive) {
                            conn_reset_for_next_request(c);
                            update_epoll_events(epfd, fd, EPOLLIN | EPOLLRDHUP, EPOLL_CTL_MOD);
                        } else {
                            c->state = ST_CLOSED;
                        }
                    }
                }
            }

            if (c->state == ST_CLOSED) {
                /* Cleanup connection */
                if (c->file_fd != -1) {
                    close(c->file_fd);
                }
                update_epoll_events(epfd, fd, 0, EPOLL_CTL_DEL);
                close(fd);
                free(c);
                conns[fd] = NULL;
            }
        }
    }

    close(epfd);
    close(listen_fd);
    return 0;
}

